package com.timk.goserver.client.board;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.timk.goserver.client.compatibility.Point;
import com.timk.goserver.client.model.ClientGameInfo;
import com.timk.goserver.client.sgf.SGFNode;
import com.timk.goserver.client.sgf.SGFParseException;
import com.timk.goserver.client.sgf.SGFParser;
import com.timk.goserver.client.sgf.SGFUtils;
import com.timk.goserver.client.ui.screens.BoardPanelListener;

/**
 * BoardController handles navigation within the game record.
 * @author TKington
 *
 */
public class BoardController implements BoardPanelListener {
	/** Coordinate for a pass */
	public static final int PASS_COORD = 19;
	
	private Board board;
	private int movesPlayed = 0;
	private int numFinalMoves;
	private int numCapturedByBlack;
	private int numCapturedByWhite;
	private SGFNode rootNode;
	final private List moves = new ArrayList();
	private boolean scoringMode;
	private int whiteScore;
	private int blackScore;
	
	/**
	 * Loads a game, and plays all moves on the Board
	 * @param game the ClientGameInfo
	 * @return the new Board
	 */
	public Board loadGame(ClientGameInfo game) {
		return loadGame(game.getSize(), game.getSgf(), game.getDeadStones());
	}
	
	/**
	 * Loads a game, and plays all moves on the Board.
	 * @param size the board size
	 * @param sgf game record
	 * @param deadStones the dead stones
	 * @return the new Board
	 */
    public Board loadGame(int size, String sgf, String deadStones) {
  	  	List games = null;
  	  	try {
  	  		games = SGFParser.parse(sgf);
  	  	}
  	  	catch(SGFParseException e) {
  	  		return null;
  	  	}
    	
  	  	board = new Board(size);
  	  	moves.clear();
  	  	movesPlayed = 0;
  	  	numCapturedByBlack = 0;
  	  	numCapturedByWhite = 0;
  	  	scoringMode = false;
  	  	
  	  	AbstractCommand prevMove = null;
    	rootNode = (SGFNode)games.get(0);
    	
    	SGFNode curNode = rootNode.getFirstChild();
    	
    	while(curNode != null) {
    		Point point = curNode.getPoint();
    		String name = curNode.getName();
    		
    		AbstractCommand cmd;
    		if("RESULT".equals(name)) {
    			//	We don't need to reload ResultCommand nodes.
    			//	They're only created to generate the SGF at the end of the game.
    			break;
    		} else if(point.x == PASS_COORD) {
    			cmd = new PassCommand(this, board, curNode.getPlayer(),
    					prevMove, curNode.getComment());
    		} else {
    			cmd = new MoveCommand(this, board, curNode.getPlayer(), point,
    					prevMove, curNode.getComment());
    		}
    		
    		doCommand(cmd);
			
			prevMove = cmd;
    		
    		curNode = curNode.getFirstChild();
    	}
    	
    	numFinalMoves = movesPlayed;
    	
    	if(deadStones != null) {
    		PassCommand pass = (PassCommand)moves.get(movesPlayed - 1);
    		List stones = SGFUtils.stringToPoints(deadStones);
    		pass.setDeadStones(stones);
    	}
    	
    	return board;
    }
    
    /**
     * Generates the final SGF for the game by creating a ResultCommand 
     * @param result the result, e.g. &quot;B+R&quot;
     * @param komi the komi
     */
    public void setResult(String result, double komi) {
    	ResultCommand cmd = new ResultCommand(this, board, 
    			(PassCommand)moves.get(movesPlayed - 1), result, komi);
    	doCommand(cmd);
    }
    
    /**
     * Returns true if at the beginning of the game record
     * @return true if at the beginning of the game record
     */
    public boolean isAtBeginning() {
    	return movesPlayed == 0;
    }
    
    /**
     * Returns true if at the end of the game record
     * @return true if at the end of the game record
     */
    public boolean isAtEnd() {
    	return movesPlayed == moves.size();
    }
    
    /**
     * Returns true if the user has played new moves
     * @return true if the user has played new moves
     */
    public boolean hasPlayedNewMoves() {
    	return movesPlayed > numFinalMoves;
    }
    
    /**
     * Backs up one move in the game record.
     * @param fireRepaint true if the board should be redisplayed
     * @return true if successful
     *
     */
    public boolean prev(boolean fireRepaint) {
    	if(movesPlayed == 0) {
			return false;
		}
    	
    	movesPlayed--;
    	AbstractCommand prevMove = (AbstractCommand)moves.get(movesPlayed);
    	prevMove.undoIt();

    	if(fireRepaint) {
			board.fireRepaint();
		}
		
    	return true;
    }
    
    /**
     * Undoes and removes the last move
     * @return true if successful
     */
    public boolean removeLastMove() {
    	if(!prev(true)) {
			return false;
		}
    	
    	moves.remove(movesPlayed);
    	return true;
    }
    
    private void doCommand(AbstractCommand cmd) {
    	cmd.doIt();
    	moves.add(cmd);
    	movesPlayed++;
    }
    
    /**
     * Moves forward one move within the game record.
     * @param fireRepaint true if the board should be redisplayed
     * @return true if successful
     *
     */
    public boolean next(boolean fireRepaint) {
    	if(movesPlayed == moves.size()) {
			return false;
		}
    	
    	AbstractCommand move = (AbstractCommand)moves.get(movesPlayed++);
    	move.doIt();

    	if(fireRepaint) {
			board.fireRepaint();
		}
    	
    	return true;
    }
    
    /**
     * Moves to the beginning of the game.
     * @return the number of moves unplayed
     */
    public int moveToBeginning() {
    	int count = 0;
    	while(prev(false)) {
			count++;
		}
    	
    	board.fireRepaint();
    	
    	return count;
    }
    
    /**
     * Moves to the end of the game record.
     * @return the number of moves played
     */
    public int moveToEnd() {
    	int count = 0;
    	while(next(false)) {
			count++;
		}
    	
    	board.fireRepaint();
    	
    	return count;
    }
    
    /**
     * Moves to the current position in the game record.
     *
     */
    public void moveToCurpos() {
    	while(movesPlayed < numFinalMoves) {
    		next(false);
    	}
    	
    	while(movesPlayed > numFinalMoves) {
    		prev(false);
    	}
    	
    	board.fireRepaint();
    }
    
    public void onClick(int x, int y) {
    	if(scoringMode) {
    		PassCommand secondPass = (PassCommand)moves.get(movesPlayed - 1);
    		secondPass.toggleGroup(x, y);
    		board.fireRepaint();
    	} else {
			if(playStone(x, y)) {
				board.fireRepaint();
			}
    	}
	}
    
    /**
     * Returns the color of the player to make the next move
     * @return the color of the player to make the next move
     */
    public int getCurPlayer() {
    	int player = 1;
    	AbstractCommand prevMove = null;
    	if(movesPlayed > 0) {
    		prevMove = (AbstractCommand)moves.get(movesPlayed - 1);
    		player = prevMove.getPlayer() * -1;
    	}
    	
    	return player;
    }

	/**
     * Plays a stone at x,y
     * @param x the x location
     * @param y the y location
     * @return true if successful
     */
    public boolean playStone(int x, int y) {
    	if(movesPlayed < numFinalMoves) {
			return false;
		}
    	
    	int player = 1;
    	AbstractCommand prevMove = null;
    	if(movesPlayed > 0) {
    		prevMove = (AbstractCommand)moves.get(movesPlayed - 1);
    		player = prevMove.getPlayer() * -1;
    	}
    	
    	if(!board.isLegalMove(player, x, y)) {
    		return false;
    	}
    	
    	removeFutureMoves();
    	
    	Point point = new Point(x, y);
    	
    	MoveCommand move = new MoveCommand(this, board, 0, point, prevMove, null);
    	doCommand(move);
    	
    	return true;
	}
    
    /**
     * Passes
     * @return true if successful
     */
    public boolean pass() {
    	if(movesPlayed < numFinalMoves) {
			return false;
		}
    	
    	AbstractCommand prevMove = null;
    	if(movesPlayed > 0) {
    		prevMove = (AbstractCommand)moves.get(movesPlayed - 1);
    	}
    	
    	removeFutureMoves();
    	
    	PassCommand pass = new PassCommand(this, board, 0, prevMove, null);
    	doCommand(pass);
    	
    	board.fireRepaint();
    	
    	return true;
    }
    
    private void removeFutureMoves() {
    	for(int i = moves.size() - 1; i >= movesPlayed; i--) {
			moves.remove(i);
		}
    }
    
    /**
     * Sets the current score without prisoners and komi
     * @param blackScore
     * @param whiteScore
     */
    public void setScore(int blackScore, int whiteScore) {
    	this.blackScore = blackScore;
    	this.whiteScore = whiteScore;
    }
    
    /**
     * Returns Black's score
     * @return Black's score
     */
    public int getBlackScore() { return blackScore + numCapturedByBlack; }
    
    /**
     * Returns White's score, not including komi
     * @return White's score, not including komi
     */
    public int getWhiteScore() { return whiteScore + numCapturedByWhite; }
    
    /**
     * Update number of captures by Black
     * @param delta
     */
    public void changeCapturedByBlack(int delta) { numCapturedByBlack += delta; }
    
    /**
     * Update number of captures by White
     * @param delta
     */
    public void changeCapturedByWhite(int delta) { numCapturedByWhite += delta; }
    
    /**
     * Sets scoring mode
     * @param scoringMode
     */
    public void setScoringMode(boolean scoringMode) { this.scoringMode = scoringMode; }
    
    /**
     * Returns true if the controller is in scoring mode
     * @return true if the controller is in scoring mode
     */
    public boolean isScoring() { return scoringMode; }
    
    /**
     * Returns the Board.
     * @return the Board
     */
    public Board getBoard() { return board; }
    
    /**
     * Returns the number of finalized moves
     * @return the number of finalized moves
     */
    public int getNumFinalMoves() { return numFinalMoves; }
    
    /**
     * Returns the number of moves played
     * @return the number of moves played
     */
    public int getMoveNumber() { return movesPlayed; }

    /**
     * Returns the number of stones captured by Black
     * @return the number of stones captured by Black
     */
    public int getNumCapturedByBlack() { return numCapturedByBlack; }
    
    /**
     * Returns the number of stones captured by White
     * @return the number of stones captured by White
     */
    public int getNumCapturedByWhite() { return numCapturedByWhite; }
    
    /**
     * Returns true if at the current position
     * @return true if at the current position
     */
    public boolean isAtCurpos() { return movesPlayed == numFinalMoves; }
    
    /**
     * Returns the new moves that have been made
     * @return ArrayList&lt;Point&gt; containing the new moves
     */
    public List getNewMoves() {
    	if(numFinalMoves >= movesPlayed) {
			return null;
		}
    	
    	List newMoves = new ArrayList();
    	for(int i = numFinalMoves; i < movesPlayed; i++) {
    		AbstractCommand move = (AbstractCommand)moves.get(i);
    		newMoves.add(move.getLocation());
    	}
    	
    	return newMoves;
    }
    
    /**
     * Returns the dead stones
     * @return Set&lt;Point&gt;
     */
    public Set getDeadStones() {
    	if(!scoringMode) {
			return null;
		}
    	
    	return ((PassCommand)moves.get(movesPlayed - 1)).getDeadStones();
    }
    
    /**
     * Returns true if the last move was a pass
     * @return true if the last move was a pass
     */
    public boolean lastMoveWasPass() {
    	if(movesPlayed == 0) {
			return false;
		}
    	
    	return moves.get(movesPlayed - 1) instanceof PassCommand;
    }
    
    /**
     * Sets the comment on the current move
     * @param curUser the current player
     * @param comment
     */
    public void setComment(String curUser, String comment) {
    	if(comment == null || movesPlayed == 0) {
    		return;
    	}
    	
    	String cmt = comment.trim();
    	if(cmt.length() < 1) {
    		return;
    	}
    	
    	AbstractCommand cmd = (AbstractCommand)moves.get(movesPlayed - 1);
    	cmd.setComment(curUser + ": " + comment);
    }
    
    /**
     * Returns the current game in SGF format
     * @return the current game in SGF format
     */
    public String getSgf() {
    	StringBuffer buf = new StringBuffer("(;");
    	
    	Map props = rootNode.getProperties();
    	for (Iterator iter = props.keySet().iterator(); iter.hasNext();) {
			String key = (String) iter.next();
			String value = (String) props.get(key);
			buf.append(key).append('[').append(value).append(']');
		}
    	
    	for (Iterator iter = moves.iterator(); iter.hasNext();) {
			AbstractCommand info = (AbstractCommand) iter.next();
			info.toSgf(buf);
		}
    	
    	buf.append(')');
    	return buf.toString();
    }
    
    /**
     * Returns the comment for the current move
     * @return the comment for the current move
     */
    public String getComment() {
    	if(movesPlayed < 1) {
    		return "";
    	}
    	
    	AbstractCommand cmd = (AbstractCommand)moves.get(movesPlayed - 1);
    	String comment = cmd.getComment();
    	
    	if(comment == null) {
    		return "";
    	}
    	
    	return comment;
    }
    
    /**
     * Returns the comments made since a specific move
     * @param moveNum the move number
     * @return the comments
     */
    public String getCommentsSince(int moveNum) {
    	StringBuffer comments = new StringBuffer(50);
    	for(int i = moveNum + 1; i < moves.size(); i++) {
    		AbstractCommand move = (AbstractCommand)moves.get(i);
    		String comment = move.getComment();
    		
    		if(comment != null && comment.length() > 0) {
    			comments.append(comment).append('\n');
    		}
    	}
    	
    	return comments.toString();
    }
}
